import http.server
import socketserver
import os
import json
from urllib.parse import unquote
import mimetypes
import qrcode
from io import BytesIO
import base64
import socket
import math
import warnings

# 忽略cgi模块的弃用警告
warnings.filterwarnings("ignore", category=DeprecationWarning, message="'cgi' is deprecated")
import cgi


# -------------------------- 配置文件加载逻辑 --------------------------
def load_config():
    """加载JSON配置文件，若不存在则生成默认配置"""
    # 配置文件路径（与脚本同目录）
    config_path = os.path.join(os.getcwd(), "config.json")
    
    # 默认配置（当配置文件不存在或字段缺失时使用）
    default_config = {
        "port": 8000,                  # 服务器端口
        "file_dir": "./files",         # 文件存储目录（相对路径/绝对路径均可）
        "max_file_size": 2147483648    # 最大文件上传限制（2GB，单位：字节）
    }

    # 检查配置文件是否存在
    if not os.path.exists(config_path):
        # 生成默认配置文件
        with open(config_path, "w", encoding="utf-8") as f:
            json.dump(default_config, f, ensure_ascii=False, indent=4)
        print(f"⚠️  未找到配置文件，已自动生成默认配置：{config_path}")
        return default_config
    
    # 读取现有配置文件
    try:
        with open(config_path, "r", encoding="utf-8") as f:
            user_config = json.load(f)
        # 合并配置（确保默认字段不缺失，用户配置优先级更高）
        merged_config = {**default_config, **user_config}
        # 处理文件路径（转为绝对路径，避免相对路径歧义）
        merged_config["file_dir"] = os.path.abspath(merged_config["file_dir"])
        return merged_config
    except json.JSONDecodeError:
        print(f"❌ 配置文件格式错误（JSON语法错误），将使用默认配置")
        return default_config
    except Exception as e:
        print(f"❌ 加载配置文件失败：{str(e)}，将使用默认配置")
        return default_config


# 加载配置并初始化核心参数
config = load_config()
PORT = config["port"]                  # 服务器端口
FILE_DIR = config["file_dir"]          # 文件存储绝对路径
MAX_FILE_SIZE = config["max_file_size"]# 最大文件上传限制（2GB）

# 确保文件存储目录存在（不存在则自动创建）
os.makedirs(FILE_DIR, exist_ok=True)


# -------------------------- 工具函数 --------------------------
def get_local_ip():
    """获取本地局域网IP地址（用于手机扫码访问）"""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))  # 连接公共DNS服务器（无需实际通信，仅用于获取本地IP）
        ip = s.getsockname()[0]
        s.close()
        return ip
    except:
        return "127.0.0.1"  # 失败时返回本地回环地址


def generate_qrcode(data):
    """生成二维码并返回Base64编码字符串（用于HTML页面显示）"""
    try:
        qr = qrcode.QRCode(
            version=1,
            error_correction=qrcode.constants.ERROR_CORRECT_L,  # 低纠错级别（二维码更简洁）
            box_size=10,
            border=4,
        )
        qr.add_data(data)
        qr.make(fit=True)
        
        img = qr.make_image(fill_color="black", back_color="white")
        buffer = BytesIO()
        img.save(buffer, format="PNG")
        return base64.b64encode(buffer.getvalue()).decode()
    except Exception as e:
        print(f"生成二维码出错: {e}")
        return None


def format_file_size(size_bytes):
    """格式化文件大小（B → KB → MB → GB，保留2位小数）"""
    if size_bytes == 0:
        return "0 B"
    size_name = ("B", "KB", "MB", "GB")
    i = int(math.floor(math.log(size_bytes, 1024)))  # 计算量级（1024进制）
    p = math.pow(1024, i)
    s = round(size_bytes / p, 2)
    return f"{s} {size_name[i]}"


def get_file_icon(file_ext):
    """根据文件扩展名返回Font Awesome图标类（用于美化文件展示）"""
    file_ext = file_ext.lower()  # 统一转为小写，避免大小写问题
    
    # 文档类
    if file_ext in ['.txt', '.doc', '.docx', '.pdf', '.rtf', '.wps', '.epub']:
        return 'fa-file-text-o'
    # 压缩包类
    elif file_ext in ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2']:
        return 'fa-file-archive-o'
    # 图片类
    elif file_ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']:
        return 'fa-file-image-o'
    # 视频类
    elif file_ext in ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv']:
        return 'fa-file-video-o'
    # 音频类
    elif file_ext in ['.mp3', '.wav', '.flac', '.m4a', '.ogg']:
        return 'fa-file-audio-o'
    # 代码类
    elif file_ext in ['.py', '.html', '.css', '.js', '.java', '.cpp', '.c', '.php']:
        return 'fa-file-code-o'
    # 表格类
    elif file_ext in ['.xls', '.xlsx', '.csv', '.numbers']:
        return 'fa-file-excel-o'  # 修复了这里的拼写错误
    # 演示文稿类
    elif file_ext in ['.ppt', '.pptx', '.key']:
        return 'fa-file-powerpoint-o'
    # 思维导图类
    elif file_ext in ['.xmind', '.mindnode']:
        return 'fa-sitemap'
    # 未知类型
    else:
        return 'fa-file-o'


# -------------------------- 核心请求处理类 --------------------------
class FileHandler(http.server.SimpleHTTPRequestHandler):
    def translate_path(self, path):
        """重写路径转换逻辑，确保文件读写指向配置的存储目录"""
        path = unquote(path)  # 解码URL中的特殊字符（如空格）
        if path == '/':  # 根路径请求（访问首页）
            return FILE_DIR
        # 非根路径（访问具体文件），拼接存储目录和文件名
        return os.path.join(FILE_DIR, os.path.basename(path))

    def do_GET(self):
        """处理GET请求（访问首页、下载文件、预览图片等）"""
        if self.path == '/':  # 访问首页，返回文件管理页面
            local_ip = get_local_ip()
            server_url = f"http://{local_ip}:{PORT}"  # 局域网访问地址
            qr_code_base64 = generate_qrcode(server_url)  # 生成访问二维码
            
            # 生成已上传文件的展示卡片
            file_grid = ""
            for filename in os.listdir(FILE_DIR):
                filepath = os.path.join(FILE_DIR, filename)
                if os.path.isfile(filepath):  # 只处理文件（跳过子目录）
                    file_ext = os.path.splitext(filename)[1]  # 获取文件扩展名（如.txt）
                    file_size = os.path.getsize(filepath)
                    formatted_size = format_file_size(file_size)
                    
                    # 判断是否为图片文件（用于预览）
                    is_image = mimetypes.guess_type(filename)[0] and mimetypes.guess_type(filename)[0].startswith('image/')
                    
                    if is_image:
                        # 图片文件：显示缩略图预览
                        file_preview = f'''
                        <div class="image-container">
                            <img src="/{filename}" class="image-preview" alt="{filename}">
                        </div>
                        '''
                    else:
                        # 非图片文件：显示对应图标
                        icon_class = get_file_icon(file_ext)
                        file_preview = f'''
                        <div class="icon-container">
                            <i class="fa {icon_class}" aria-hidden="true"></i>
                            <span class="file-type">{file_ext[1:].upper() or '文件'}</span>
                        </div>
                        '''
                    
                    # 拼接单个文件卡片HTML
                    file_grid += f'''
                    <div class="file-card">
                        <a href="/{filename}" target="_blank" download class="file-link">
                            {file_preview}
                            <div class="file-info">
                                <div class="file-name" title="{filename}">{filename}</div>
                                <div class="file-size">{formatted_size}</div>
                            </div>
                        </a>
                    </div>
                    '''

            # 构建完整的首页HTML（包含样式、二维码、上传表单、文件列表）
            html = f'''
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <!-- 适配手机端 -->
                <title>文件传输服务器</title>
                <!-- 引入Font Awesome图标库（CDN） -->
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
                <style>
                    * {{
                        box-sizing: border-box;
                        margin: 0;
                        padding: 0;
                    }}
                    
                    body {{ 
                        font-family: 'Segoe UI', Arial, sans-serif; 
                        max-width: 1400px; 
                        margin: 0 auto; 
                        padding: 20px;
                        background-color: #f5f7fa;
                        color: #333;
                    }}
                    
                    h1 {{ 
                        color: #2c3e50;
                        margin-bottom: 20px;
                        font-weight: 600;
                    }}
                    
                    .qrcode-section {{ 
                        margin: 20px 0; 
                        padding: 20px; 
                        background-color: #ffffff; 
                        border-radius: 10px;
                        box-shadow: 0 2px 8px rgba(0,0,0,0.05);
                    }}
                    
                    .qrcode-container {{ 
                        display: inline-block; 
                        padding: 15px; 
                        background: white; 
                        border-radius: 8px; 
                        box-shadow: 0 2px 4px rgba(0,0,0,0.1);
                    }}
                    
                    .upload-form {{ 
                        margin: 20px 0; 
                        padding: 25px; 
                        background-color: #ffffff; 
                        border-radius: 10px;
                        box-shadow: 0 2px 8px rgba(0,0,0,0.05);
                    }}
                    
                    .file-input {{ 
                        padding: 10px 20px; 
                        font-size: 16px; 
                        margin-right: 10px;
                        border: 1px solid #ddd;
                        border-radius: 6px;
                        background-color: #f9f9f9;
                    }}
                    
                    .submit-btn {{ 
                        padding: 10px 24px; 
                        font-size: 16px; 
                        background-color: #3498db; 
                        color: white; 
                        border: none; 
                        border-radius: 6px; 
                        cursor: pointer;
                        transition: background-color 0.3s;
                    }}
                    
                    .submit-btn:hover {{ 
                        background-color: #2980b9;
                    }}
                    
                    .file-grid {{ 
                        display: grid; 
                        grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); 
                        gap: 16px; 
                        margin-top: 20px;
                    }}
                    
                    .file-card {{ 
                        background-color: #ffffff;
                        border-radius: 10px;
                        overflow: hidden;
                        box-shadow: 0 2px 8px rgba(0,0,0,0.05);
                        transition: transform 0.2s, box-shadow 0.2s;
                    }}
                    
                    .file-card:hover {{
                        transform: translateY(-3px);
                        box-shadow: 0 5px 15px rgba(0,0,0,0.1);
                    }}
                    
                    .file-link {{
                        text-decoration: none;
                        color: inherit;
                        display: flex;
                        flex-direction: column;
                        height: 100%;
                    }}
                    
                    .image-container {{
                        width: 100%;
                        height: 140px;
                        overflow: hidden;
                        background-color: #f5f5f5;
                        display: flex;
                        align-items: center;
                        justify-content: center;
                    }}
                    
                    .image-preview {{ 
                        max-width: 100%; 
                        max-height: 100%; 
                        object-fit: contain;
                        transition: transform 0.3s;
                    }}
                    
                    .file-card:hover .image-preview {{
                        transform: scale(1.05);
                    }}
                    
                    .icon-container {{ 
                        width: 100%;
                        height: 140px;
                        background-color: #f8f9fa;
                        display: flex;
                        flex-direction: column;
                        align-items: center;
                        justify-content: center;
                    }}
                    
                    .icon-container i {{
                        font-size: 48px;
                        color: #3498db;
                        margin-bottom: 10px;
                    }}
                    
                    .file-type {{
                        font-size: 13px;
                        color: #666;
                        text-transform: uppercase;
                    }}
                    
                    .file-info {{
                        padding: 12px;
                        flex-grow: 1;
                        display: flex;
                        flex-direction: column;
                    }}
                    
                    .file-name {{ 
                        font-size: 14px;
                        margin-bottom: 6px;
                        color: #333;
                        word-break: break-word;
                        flex-grow: 1;
                        line-height: 1.4;
                        max-height: 4em;
                        overflow: hidden;
                    }}
                    
                    .file-size {{ 
                        font-size: 12px; 
                        color: #7f8c8d;
                        text-align: right;
                    }}
                    
                    hr {{
                        border: none;
                        height: 1px;
                        background-color: #eee;
                        margin: 20px 0;
                    }}
                    
                    /* 上传进度条样式 */
                    .progress-container {{
                        margin-top: 15px;
                        display: none;
                    }}
                    
                    .progress-bar {{
                        height: 20px;
                        background-color: #f0f0f0;
                        border-radius: 10px;
                        overflow: hidden;
                        position: relative;
                    }}
                    
                    .progress-fill {{
                        height: 100%;
                        background-color: #4CAF50;
                        width: 0%;
                        transition: width 0.3s;
                    }}
                    
                    .progress-text {{
                        position: absolute;
                        top: 0;
                        left: 0;
                        width: 100%;
                        text-align: center;
                        line-height: 20px;
                        color: #333;
                        font-size: 12px;
                    }}
                </style>
            </head>
            <body>
                <h1>文件传输服务器</h1>
                
                <div class="qrcode-section">
                    <h3>手机扫码访问</h3>
                    <div class="qrcode-container">
                        {f'<img src="data:image/png;base64,{qr_code_base64}" alt="访问二维码">' if qr_code_base64 else '<p>无法生成二维码</p>'}
                    </div>
                    <p style="margin-top: 10px;">服务器地址: {server_url}</p>
                    <p style="margin-top: 5px; color: #666; font-size: 14px;">存储目录: {FILE_DIR}</p>
                </div>
                
                <div class="upload-form">
                    <form method="POST" enctype="multipart/form-data" id="uploadForm">
                        <input type="file" name="files" multiple class="file-input" id="fileInput">
                        <input type="submit" value="上传文件" class="submit-btn">
                        <p style="margin-top: 10px; color: #666; font-size: 14px;">
                            最大支持文件大小: {format_file_size(MAX_FILE_SIZE)}
                        </p>
                        <div class="progress-container" id="progressContainer">
                            <div class="progress-bar">
                                <div class="progress-fill" id="progressFill"></div>
                                <div class="progress-text" id="progressText">0%</div>
                            </div>
                        </div>
                    </form>
                </div>
                
                <hr>
                <h3>已上传文件</h3>
                <div class="file-grid">
                    {file_grid if file_grid else '<p style="color: #666;">暂无上传文件</p>'}
                </div>
                
                <script>
                    // 上传进度显示脚本
                    document.getElementById('uploadForm').addEventListener('submit', function(e) {{
                        const fileInput = document.getElementById('fileInput');
                        const progressContainer = document.getElementById('progressContainer');
                        const progressFill = document.getElementById('progressFill');
                        const progressText = document.getElementById('progressText');
                        
                        if (fileInput.files.length > 0) {{
                            // 显示进度条
                            progressContainer.style.display = 'block';
                            
                            // 创建FormData对象
                            const formData = new FormData(this);
                            
                            // 创建XMLHttpRequest对象
                            const xhr = new XMLHttpRequest();
                            
                            // 监听上传进度
                            xhr.upload.addEventListener('progress', function(e) {{
                                if (e.lengthComputable) {{
                                    const percent = Math.round((e.loaded / e.total) * 100);
                                    progressFill.style.width = percent + '%';
                                    progressText.textContent = percent + '%';
                                }}
                            }});
                            
                            // 上传完成后的处理
                            xhr.addEventListener('load', function() {{
                                if (xhr.status === 200) {{
                                    // 上传成功，刷新页面
                                    window.location.reload();
                                }} else {{
                                    // 上传失败，显示错误信息
                                    alert('上传失败: ' + xhr.responseText);
                                    progressContainer.style.display = 'none';
                                }}
                            }});
                            
                            // 发送请求
                            xhr.open('POST', '/', true);
                            xhr.send(formData);
                            
                            // 阻止表单默认提交行为
                            e.preventDefault();
                        }}
                    }});
                </script>
            </body>
            </html>
            '''

            # 发送HTML响应
            self.send_response(200)
            self.send_header('Content-type', 'text/html; charset=utf-8')
            self.end_headers()
            self.wfile.write(html.encode('utf-8'))
        else:
            # 非首页请求（下载/预览文件），使用父类默认逻辑
            super().do_GET()

    def do_POST(self):
        """处理POST请求（文件上传）"""
        # 检查请求大小是否超过限制
        content_length = int(self.headers.get('Content-Length', 0))
        if content_length > MAX_FILE_SIZE:
            self.send_response(413)  # 413 Request Entity Too Large
            self.send_header('Content-type', 'text/html; charset=utf-8')
            self.end_headers()
            self.wfile.write(f'''
                <html><body>
                    <h1>文件过大</h1>
                    <p>最大支持文件大小: {format_file_size(MAX_FILE_SIZE)}</p>
                    <a href="/">返回首页</a>
                </body></html>
            '''.encode('utf-8'))
            return

        # 使用cgi.FieldStorage处理多部分表单数据（支持大文件流式处理）
        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={'REQUEST_METHOD': 'POST',
                     'CONTENT_TYPE': self.headers['Content-Type']}
        )
        
        # 遍历所有上传的文件
        for field in form.list:
            if field.filename:
                # 获取文件名（处理中文和特殊字符）
                filename = field.filename
                if not filename:  # 跳过空文件名
                    continue
                
                # 创建目标文件路径
                save_path = os.path.join(FILE_DIR, filename)
                
                # 流式写入文件（避免内存溢出）
                try:
                    with open(save_path, 'wb') as f:
                        # 分块读取并写入文件
                        while True:
                            chunk = field.file.read(8192)  # 8KB块大小
                            if not chunk:
                                break
                            f.write(chunk)
                    
                    # 获取文件大小
                    file_size = os.path.getsize(save_path)
                    print(f"✅ 已上传文件: {filename}（大小: {format_file_size(file_size)}）")
                except Exception as e:
                    print(f"❌ 文件上传失败: {filename} - {str(e)}")
                    # 删除可能已部分写入的文件
                    if os.path.exists(save_path):
                        os.remove(save_path)
                    # 返回错误响应
                    self.send_response(500)
                    self.send_header('Content-type', 'text/plain; charset=utf-8')
                    self.end_headers()
                    self.wfile.write(f"文件上传失败: {str(e)}".encode('utf-8'))
                    return

        # 上传成功，返回200状态码（AJAX请求会刷新页面）
        self.send_response(200)
        self.send_header('Content-type', 'text/plain; charset=utf-8')
        self.end_headers()
        self.wfile.write("文件上传成功".encode('utf-8'))


# -------------------------- 服务器启动入口 --------------------------
if __name__ == '__main__':
    local_ip = get_local_ip()
    # 启动TCP服务器（支持多线程，避免单次请求阻塞）
    with socketserver.ThreadingTCPServer(("", PORT), FileHandler) as httpd:
        print(f"🚀 文件服务器已启动！")
        print(f"📂 存储目录：{FILE_DIR}")
        print(f"🌐 本地访问：http://localhost:{PORT}")
        print(f"📱 局域网访问：http://{local_ip}:{PORT}")
        print(f"📌 最大文件限制：{format_file_size(MAX_FILE_SIZE)}")
        try:
            httpd.serve_forever()  # 持续运行服务器
        except KeyboardInterrupt:
            print("\n🛑 服务器已手动停止")